route.ts 2.2 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859
  1. import { NextRequest, NextResponse } from 'next/server';
  2. import { fetchJson } from '@/lib/utils/server';
  3. import { isSocialLoginProvider, SOCIAL_PROVIDER_LABELS, OAUTH_STATE_COOKIE_PREFIX } from '@/constants/social';
  4. /**
  5. * 소셜 로그인 시작 라우트 (Naver/Kakao) — /login/{provider}/start
  6. *
  7. * 1. state 생성 (crypto.randomUUID) → httpOnly 쿠키 보관 (10분)
  8. * 2. 백엔드 GET /api/auth/{provider}/authorize-url 호출
  9. * 3. 각사 authorize 페이지로 302 리다이렉트
  10. *
  11. * Google 은 기존 GIS(id_token) 플로우(/login/google/*) 유지 — 이 라우트 미사용.
  12. */
  13. interface AuthorizeUrlResponse {
  14. provider: string;
  15. authorizeUrl: string;
  16. }
  17. export async function GET(request: NextRequest, { params }: { params: Promise<{ provider: string }> })
  18. {
  19. const { provider } = await params;
  20. const failRedirect = (message: string) => {
  21. const url = new URL('/login', request.url);
  22. url.searchParams.set('error', message);
  23. return NextResponse.redirect(url);
  24. };
  25. if (!isSocialLoginProvider(provider)) {
  26. return failRedirect('지원하지 않는 소셜 로그인입니다.');
  27. }
  28. const label = SOCIAL_PROVIDER_LABELS[provider];
  29. const state = crypto.randomUUID();
  30. const redirectUri = `${request.nextUrl.origin}/login/${provider}/callback`;
  31. const query = new URLSearchParams({ state, redirectUri });
  32. const res = await fetchJson<AuthorizeUrlResponse>(`/api/auth/${provider}/authorize-url?${query.toString()}`);
  33. if (!res.success || !res.data?.authorizeUrl) {
  34. // OAuth 앱 키 미설정(Auth.SocialNotConfigured)은 사용자 친화 문구로 치환
  35. if (res.errors?.some(e => e.code === 'Auth.SocialNotConfigured')) {
  36. return failRedirect(`${label} 로그인이 아직 준비되지 않았습니다. 잠시 후 다시 시도해주세요.`);
  37. }
  38. return failRedirect(res.errors?.[0]?.description || res.message || `${label} 로그인을 시작할 수 없습니다. 잠시 후 다시 시도해주세요.`);
  39. }
  40. const response = NextResponse.redirect(res.data.authorizeUrl);
  41. response.cookies.set(`${OAUTH_STATE_COOKIE_PREFIX}${provider}`, state, {
  42. httpOnly: true,
  43. path: '/',
  44. sameSite: 'lax',
  45. secure: process.env.NODE_ENV === 'production',
  46. maxAge: 600
  47. });
  48. return response;
  49. }